aboutsummaryrefslogtreecommitdiff
path: root/src/app/(main)/pixels/[pixelId]
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-24 13:09:50 +0000
committerFuwn <[email protected]>2026-01-24 13:09:50 +0000
commit396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch)
treeb9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/pixels/[pixelId]
downloadumami-main.tar.xz
umami-main.zip
Initial commitHEADmain
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/pixels/[pixelId]')
-rw-r--r--src/app/(main)/pixels/[pixelId]/PixelControls.tsx32
-rw-r--r--src/app/(main)/pixels/[pixelId]/PixelHeader.tsx19
-rw-r--r--src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx70
-rw-r--r--src/app/(main)/pixels/[pixelId]/PixelPage.tsx34
-rw-r--r--src/app/(main)/pixels/[pixelId]/PixelPanels.tsx83
-rw-r--r--src/app/(main)/pixels/[pixelId]/page.tsx12
6 files changed, 250 insertions, 0 deletions
diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
new file mode 100644
index 0000000..55dcd57
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
@@ -0,0 +1,32 @@
+import { Column, Row } from '@umami/react-zen';
+import { ExportButton } from '@/components/input/ExportButton';
+import { FilterBar } from '@/components/input/FilterBar';
+import { MonthFilter } from '@/components/input/MonthFilter';
+import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
+import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+
+export function PixelControls({
+ pixelId: websiteId,
+ allowFilter = true,
+ allowDateFilter = true,
+ allowMonthFilter,
+ allowDownload = false,
+}: {
+ pixelId: string;
+ allowFilter?: boolean;
+ allowDateFilter?: boolean;
+ allowMonthFilter?: boolean;
+ allowDownload?: boolean;
+}) {
+ return (
+ <Column gap>
+ <Row alignItems="center" justifyContent="space-between" gap="3">
+ {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
+ {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
+ {allowDownload && <ExportButton websiteId={websiteId} />}
+ {allowMonthFilter && <MonthFilter />}
+ </Row>
+ {allowFilter && <FilterBar websiteId={websiteId} />}
+ </Column>
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
new file mode 100644
index 0000000..c771687
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
@@ -0,0 +1,19 @@
+import { IconLabel } from '@umami/react-zen';
+import { LinkButton } from '@/components/common/LinkButton';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages, usePixel, useSlug } from '@/components/hooks';
+import { ExternalLink, Grid2x2 } from '@/components/icons';
+
+export function PixelHeader() {
+ const { formatMessage, labels } = useMessages();
+ const { getSlugUrl } = useSlug('pixel');
+ const pixel = usePixel();
+
+ return (
+ <PageHeader title={pixel.name} icon={<Grid2x2 />}>
+ <LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false} asAnchor>
+ <IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
+ </LinkButton>
+ </PageHeader>
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
new file mode 100644
index 0000000..c9dcd35
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
@@ -0,0 +1,70 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongNumber } from '@/lib/format';
+
+export function PixelMetricsBar({
+ pixelId,
+}: {
+ pixelId: string;
+ showChange?: boolean;
+ compareMode?: boolean;
+}) {
+ const { isAllTime } = useDateRange();
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
+
+ const { pageviews, visitors, visits, comparison } = data || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ change: visitors - comparison.visitors,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ change: visits - comparison.visits,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ change: pageviews - comparison.pageviews,
+ formatValue: formatLongNumber,
+ },
+ ]
+ : null;
+
+ return (
+ <LoadingPanel
+ data={metrics}
+ isLoading={isLoading}
+ isFetching={isFetching}
+ error={error}
+ minHeight="136px"
+ >
+ <MetricsBar>
+ {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
+ return (
+ <MetricCard
+ key={label}
+ value={value}
+ previousValue={prev}
+ label={label}
+ change={change}
+ formatValue={formatValue}
+ reverseColors={reverseColors}
+ showChange={!isAllTime}
+ />
+ );
+ })}
+ </MetricsBar>
+ </LoadingPanel>
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
new file mode 100644
index 0000000..7a4ae9d
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
@@ -0,0 +1,34 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
+import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader';
+import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
+import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
+import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
+import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
+import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
+import { PageBody } from '@/components/common/PageBody';
+import { Panel } from '@/components/common/Panel';
+
+const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
+
+export function PixelPage({ pixelId }: { pixelId: string }) {
+ return (
+ <PixelProvider pixelId={pixelId}>
+ <Grid width="100%" height="100%">
+ <Column margin="2">
+ <PageBody gap>
+ <PixelHeader />
+ <PixelControls pixelId={pixelId} />
+ <PixelMetricsBar pixelId={pixelId} showChange={true} />
+ <Panel>
+ <WebsiteChart websiteId={pixelId} />
+ </Panel>
+ <PixelPanels pixelId={pixelId} />
+ </PageBody>
+ <ExpandedViewModal websiteId={pixelId} excludedIds={excludedIds} />
+ </Column>
+ </Grid>
+ </PixelProvider>
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
new file mode 100644
index 0000000..9cc24c9
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
@@ -0,0 +1,83 @@
+import { Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { GridRow } from '@/components/common/GridRow';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { WorldMap } from '@/components/metrics/WorldMap';
+
+export function PixelPanels({ pixelId }: { pixelId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const tableProps = {
+ websiteId: pixelId,
+ limit: 10,
+ allowDownload: false,
+ showMore: true,
+ metric: formatMessage(labels.visitors),
+ };
+ const rowProps = { minHeight: 570 };
+
+ return (
+ <Grid gap="3">
+ <GridRow layout="two" {...rowProps}>
+ <Panel>
+ <Heading size="2">{formatMessage(labels.sources)}</Heading>
+ <Tabs>
+ <TabList>
+ <Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
+ <Tab id="channel">{formatMessage(labels.channels)}</Tab>
+ </TabList>
+ <TabPanel id="referrer">
+ <MetricsTable type="referrer" title={formatMessage(labels.domain)} {...tableProps} />
+ </TabPanel>
+ <TabPanel id="channel">
+ <MetricsTable type="channel" title={formatMessage(labels.type)} {...tableProps} />
+ </TabPanel>
+ </Tabs>
+ </Panel>
+ <Panel>
+ <Heading size="2">{formatMessage(labels.environment)}</Heading>
+ <Tabs>
+ <TabList>
+ <Tab id="browser">{formatMessage(labels.browsers)}</Tab>
+ <Tab id="os">{formatMessage(labels.os)}</Tab>
+ <Tab id="device">{formatMessage(labels.devices)}</Tab>
+ </TabList>
+ <TabPanel id="browser">
+ <MetricsTable type="browser" title={formatMessage(labels.browser)} {...tableProps} />
+ </TabPanel>
+ <TabPanel id="os">
+ <MetricsTable type="os" title={formatMessage(labels.os)} {...tableProps} />
+ </TabPanel>
+ <TabPanel id="device">
+ <MetricsTable type="device" title={formatMessage(labels.device)} {...tableProps} />
+ </TabPanel>
+ </Tabs>
+ </Panel>
+ </GridRow>
+ <GridRow layout="two" {...rowProps}>
+ <Panel padding="0">
+ <WorldMap websiteId={pixelId} />
+ </Panel>
+ <Panel>
+ <Heading size="2">{formatMessage(labels.location)}</Heading>
+ <Tabs>
+ <TabList>
+ <Tab id="country">{formatMessage(labels.countries)}</Tab>
+ <Tab id="region">{formatMessage(labels.regions)}</Tab>
+ <Tab id="city">{formatMessage(labels.cities)}</Tab>
+ </TabList>
+ <TabPanel id="country">
+ <MetricsTable type="country" title={formatMessage(labels.country)} {...tableProps} />
+ </TabPanel>
+ <TabPanel id="region">
+ <MetricsTable type="region" title={formatMessage(labels.region)} {...tableProps} />
+ </TabPanel>
+ <TabPanel id="city">
+ <MetricsTable type="city" title={formatMessage(labels.city)} {...tableProps} />
+ </TabPanel>
+ </Tabs>
+ </Panel>
+ </GridRow>
+ </Grid>
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/page.tsx b/src/app/(main)/pixels/[pixelId]/page.tsx
new file mode 100644
index 0000000..d1db92f
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { PixelPage } from './PixelPage';
+
+export default async function ({ params }: { params: { pixelId: string } }) {
+ const { pixelId } = await params;
+
+ return <PixelPage pixelId={pixelId} />;
+}
+
+export const metadata: Metadata = {
+ title: 'Pixel',
+};